// Copyright (c) 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ui/message_center/views/message_popup_collection.h"

#include <stddef.h>

#include <list>
#include <utility>

#include "base/message_loop/message_loop.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "build/build_config.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/events/event.h"
#include "ui/events/event_constants.h"
#include "ui/events/event_utils.h"
#include "ui/gfx/display.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/message_center/fake_message_center.h"
#include "ui/message_center/views/desktop_popup_alignment_delegate.h"
#include "ui/message_center/views/toast_contents_view.h"
#include "ui/views/test/views_test_base.h"
#include "ui/views/widget/widget.h"
#include "ui/views/widget/widget_delegate.h"

namespace message_center {
namespace test {

    class MessagePopupCollectionTest : public views::ViewsTestBase {
    public:
        void SetUp() override
        {
            views::ViewsTestBase::SetUp();
            MessageCenter::Initialize();
            MessageCenter::Get()->DisableTimersForTest();
            alignment_delegate_.reset(new DesktopPopupAlignmentDelegate);
            collection_.reset(new MessagePopupCollection(
                GetContext(), MessageCenter::Get(), NULL, alignment_delegate_.get()));
            // This size fits test machines resolution and also can keep a few toasts
            // w/o ill effects of hitting the screen overflow. This allows us to assume
            // and verify normal layout of the toast stack.
            SetDisplayInfo(gfx::Rect(0, 0, 600, 390), // taskbar at the bottom.
                gfx::Rect(0, 0, 600, 400));
            id_ = 0;
            PrepareForWait();
        }

        void TearDown() override
        {
            collection_.reset();
            MessageCenter::Shutdown();
            views::ViewsTestBase::TearDown();
        }

    protected:
        MessagePopupCollection* collection() { return collection_.get(); }

        size_t GetToastCounts()
        {
            return collection_->toasts_.size();
        }

        bool MouseInCollection()
        {
            return collection_->latest_toast_entered_ != NULL;
        }

        bool IsToastShown(const std::string& id)
        {
            views::Widget* widget = collection_->GetWidgetForTest(id);
            return widget && widget->IsVisible();
        }

        views::Widget* GetWidget(const std::string& id)
        {
            return collection_->GetWidgetForTest(id);
        }

        void SetDisplayInfo(const gfx::Rect& work_area,
            const gfx::Rect& display_bounds)
        {
            gfx::Display dummy_display;
            dummy_display.set_bounds(display_bounds);
            dummy_display.set_work_area(work_area);
            alignment_delegate_->RecomputeAlignment(dummy_display);
            PrepareForWait();
        }

        gfx::Rect GetWorkArea()
        {
            return alignment_delegate_->work_area_;
        }

        ToastContentsView* GetToast(const std::string& id)
        {
            for (MessagePopupCollection::Toasts::iterator iter = collection_->toasts_.begin();
                 iter != collection_->toasts_.end(); ++iter) {
                if ((*iter)->id() == id)
                    return *iter;
            }
            return NULL;
        }

        std::string AddNotification()
        {
            std::string id = base::IntToString(id_++);
            scoped_ptr<Notification> notification(new Notification(
                NOTIFICATION_TYPE_BASE_FORMAT, id, base::UTF8ToUTF16("test title"),
                base::UTF8ToUTF16("test message"), gfx::Image(),
                base::string16() /* display_source */, GURL(), NotifierId(),
                message_center::RichNotificationData(), new NotificationDelegate()));
            MessageCenter::Get()->AddNotification(std::move(notification));
            return id;
        }

        void PrepareForWait() { collection_->CreateRunLoopForTest(); }

        // Assumes there is non-zero pending work.
        void WaitForTransitionsDone()
        {
            collection_->WaitForTest();
            collection_->CreateRunLoopForTest();
        }

        void CloseAllToasts()
        {
            // Assumes there is at least one toast to close.
            EXPECT_TRUE(GetToastCounts() > 0);
            MessageCenter::Get()->RemoveAllNotifications(false);
        }

        gfx::Rect GetToastRectAt(size_t index)
        {
            return collection_->GetToastRectAt(index);
        }

    private:
        scoped_ptr<MessagePopupCollection> collection_;
        scoped_ptr<DesktopPopupAlignmentDelegate> alignment_delegate_;
        int id_;
    };

#if defined(OS_CHROMEOS)
    TEST_F(MessagePopupCollectionTest, DismissOnClick)
    {

        std::string id1 = AddNotification();
        std::string id2 = AddNotification();
        WaitForTransitionsDone();

        EXPECT_EQ(2u, GetToastCounts());
        EXPECT_TRUE(IsToastShown(id1));
        EXPECT_TRUE(IsToastShown(id2));

        MessageCenter::Get()->ClickOnNotification(id2);
        WaitForTransitionsDone();

        EXPECT_EQ(1u, GetToastCounts());
        EXPECT_TRUE(IsToastShown(id1));
        EXPECT_FALSE(IsToastShown(id2));

        MessageCenter::Get()->ClickOnNotificationButton(id1, 0);
        WaitForTransitionsDone();
        EXPECT_EQ(0u, GetToastCounts());
        EXPECT_FALSE(IsToastShown(id1));
        EXPECT_FALSE(IsToastShown(id2));
    }

#else

    TEST_F(MessagePopupCollectionTest, NotDismissedOnClick)
    {
        std::string id1 = AddNotification();
        std::string id2 = AddNotification();
        WaitForTransitionsDone();

        EXPECT_EQ(2u, GetToastCounts());
        EXPECT_TRUE(IsToastShown(id1));
        EXPECT_TRUE(IsToastShown(id2));

        MessageCenter::Get()->ClickOnNotification(id2);
        collection()->DoUpdateIfPossible();

        EXPECT_EQ(2u, GetToastCounts());
        EXPECT_TRUE(IsToastShown(id1));
        EXPECT_TRUE(IsToastShown(id2));

        MessageCenter::Get()->ClickOnNotificationButton(id1, 0);
        collection()->DoUpdateIfPossible();
        EXPECT_EQ(2u, GetToastCounts());
        EXPECT_TRUE(IsToastShown(id1));
        EXPECT_TRUE(IsToastShown(id2));

        GetWidget(id1)->CloseNow();
        GetWidget(id2)->CloseNow();
    }

#endif // OS_CHROMEOS

    TEST_F(MessagePopupCollectionTest, ShutdownDuringShowing)
    {
        std::string id1 = AddNotification();
        std::string id2 = AddNotification();
        WaitForTransitionsDone();
        EXPECT_EQ(2u, GetToastCounts());
        EXPECT_TRUE(IsToastShown(id1));
        EXPECT_TRUE(IsToastShown(id2));

        // Finish without cleanup of notifications, which may cause use-after-free.
        // See crbug.com/236448
        GetWidget(id1)->CloseNow();
        collection()->OnMouseExited(GetToast(id2));

        GetWidget(id2)->CloseNow();
    }

    TEST_F(MessagePopupCollectionTest, DefaultPositioning)
    {
        std::string id0 = AddNotification();
        std::string id1 = AddNotification();
        std::string id2 = AddNotification();
        std::string id3 = AddNotification();
        WaitForTransitionsDone();

        gfx::Rect r0 = GetToastRectAt(0);
        gfx::Rect r1 = GetToastRectAt(1);
        gfx::Rect r2 = GetToastRectAt(2);
        gfx::Rect r3 = GetToastRectAt(3);

        // 3 toasts are shown, equal size, vertical stack.
        EXPECT_TRUE(IsToastShown(id0));
        EXPECT_TRUE(IsToastShown(id1));
        EXPECT_TRUE(IsToastShown(id2));

        EXPECT_EQ(r0.width(), r1.width());
        EXPECT_EQ(r1.width(), r2.width());

        EXPECT_EQ(r0.height(), r1.height());
        EXPECT_EQ(r1.height(), r2.height());

        EXPECT_GT(r0.y(), r1.y());
        EXPECT_GT(r1.y(), r2.y());

        EXPECT_EQ(r0.x(), r1.x());
        EXPECT_EQ(r1.x(), r2.x());

        // The 4th toast is not shown yet.
        EXPECT_FALSE(IsToastShown(id3));
        EXPECT_EQ(0, r3.width());
        EXPECT_EQ(0, r3.height());

        CloseAllToasts();
        EXPECT_EQ(0u, GetToastCounts());
        WaitForTransitionsDone();
    }

    TEST_F(MessagePopupCollectionTest, DefaultPositioningWithRightTaskbar)
    {
        // If taskbar is on the right we show the toasts bottom to top as usual.

        // Simulate a taskbar at the right.
        SetDisplayInfo(gfx::Rect(0, 0, 590, 400), // Work-area.
            gfx::Rect(0, 0, 600, 400)); // Display-bounds.
        std::string id0 = AddNotification();
        std::string id1 = AddNotification();
        WaitForTransitionsDone();

        gfx::Rect r0 = GetToastRectAt(0);
        gfx::Rect r1 = GetToastRectAt(1);

        // 2 toasts are shown, equal size, vertical stack.
        EXPECT_TRUE(IsToastShown(id0));
        EXPECT_TRUE(IsToastShown(id1));

        EXPECT_EQ(r0.width(), r1.width());
        EXPECT_EQ(r0.height(), r1.height());
        EXPECT_GT(r0.y(), r1.y());
        EXPECT_EQ(r0.x(), r1.x());

        CloseAllToasts();
        EXPECT_EQ(0u, GetToastCounts());

        // Restore simulated taskbar position to bottom.
        SetDisplayInfo(gfx::Rect(0, 0, 600, 390), // Work-area.
            gfx::Rect(0, 0, 600, 400)); // Display-bounds.

        WaitForTransitionsDone();
    }

    TEST_F(MessagePopupCollectionTest, TopDownPositioningWithTopTaskbar)
    {
        // Simulate a taskbar at the top.
        SetDisplayInfo(gfx::Rect(0, 10, 600, 390), // Work-area.
            gfx::Rect(0, 0, 600, 400)); // Display-bounds.
        std::string id0 = AddNotification();
        std::string id1 = AddNotification();
        WaitForTransitionsDone();

        gfx::Rect r0 = GetToastRectAt(0);
        gfx::Rect r1 = GetToastRectAt(1);

        // 2 toasts are shown, equal size, vertical stack.
        EXPECT_TRUE(IsToastShown(id0));
        EXPECT_TRUE(IsToastShown(id1));

        EXPECT_EQ(r0.width(), r1.width());
        EXPECT_EQ(r0.height(), r1.height());
        EXPECT_LT(r0.y(), r1.y());
        EXPECT_EQ(r0.x(), r1.x());

        CloseAllToasts();
        EXPECT_EQ(0u, GetToastCounts());
        WaitForTransitionsDone();

        // Restore simulated taskbar position to bottom.
        SetDisplayInfo(gfx::Rect(0, 0, 600, 390), // Work-area.
            gfx::Rect(0, 0, 600, 400)); // Display-bounds.
    }

    TEST_F(MessagePopupCollectionTest, TopDownPositioningWithLeftAndTopTaskbar)
    {
        // If there "seems" to be a taskbar on left and top (like in Unity), it is
        // assumed that the actual taskbar is the top one.

        // Simulate a taskbar at the top and left.
        SetDisplayInfo(gfx::Rect(10, 10, 590, 390), // Work-area.
            gfx::Rect(0, 0, 600, 400)); // Display-bounds.
        std::string id0 = AddNotification();
        std::string id1 = AddNotification();
        WaitForTransitionsDone();

        gfx::Rect r0 = GetToastRectAt(0);
        gfx::Rect r1 = GetToastRectAt(1);

        // 2 toasts are shown, equal size, vertical stack.
        EXPECT_TRUE(IsToastShown(id0));
        EXPECT_TRUE(IsToastShown(id1));

        EXPECT_EQ(r0.width(), r1.width());
        EXPECT_EQ(r0.height(), r1.height());
        EXPECT_LT(r0.y(), r1.y());
        EXPECT_EQ(r0.x(), r1.x());

        CloseAllToasts();
        EXPECT_EQ(0u, GetToastCounts());
        WaitForTransitionsDone();

        // Restore simulated taskbar position to bottom.
        SetDisplayInfo(gfx::Rect(0, 0, 600, 390), // Work-area.
            gfx::Rect(0, 0, 600, 400)); // Display-bounds.
    }

    TEST_F(MessagePopupCollectionTest, TopDownPositioningWithBottomAndTopTaskbar)
    {
        // If there "seems" to be a taskbar on bottom and top (like in Gnome), it is
        // assumed that the actual taskbar is the top one.

        // Simulate a taskbar at the top and bottom.
        SetDisplayInfo(gfx::Rect(0, 10, 580, 400), // Work-area.
            gfx::Rect(0, 0, 600, 400)); // Display-bounds.
        std::string id0 = AddNotification();
        std::string id1 = AddNotification();
        WaitForTransitionsDone();

        gfx::Rect r0 = GetToastRectAt(0);
        gfx::Rect r1 = GetToastRectAt(1);

        // 2 toasts are shown, equal size, vertical stack.
        EXPECT_TRUE(IsToastShown(id0));
        EXPECT_TRUE(IsToastShown(id1));

        EXPECT_EQ(r0.width(), r1.width());
        EXPECT_EQ(r0.height(), r1.height());
        EXPECT_LT(r0.y(), r1.y());
        EXPECT_EQ(r0.x(), r1.x());

        CloseAllToasts();
        EXPECT_EQ(0u, GetToastCounts());
        WaitForTransitionsDone();

        // Restore simulated taskbar position to bottom.
        SetDisplayInfo(gfx::Rect(0, 0, 600, 390), // Work-area.
            gfx::Rect(0, 0, 600, 400)); // Display-bounds.
    }

    TEST_F(MessagePopupCollectionTest, LeftPositioningWithLeftTaskbar)
    {
        // Simulate a taskbar at the left.
        SetDisplayInfo(gfx::Rect(10, 0, 590, 400), // Work-area.
            gfx::Rect(0, 0, 600, 400)); // Display-bounds.
        std::string id0 = AddNotification();
        std::string id1 = AddNotification();
        WaitForTransitionsDone();

        gfx::Rect r0 = GetToastRectAt(0);
        gfx::Rect r1 = GetToastRectAt(1);

        // 2 toasts are shown, equal size, vertical stack.
        EXPECT_TRUE(IsToastShown(id0));
        EXPECT_TRUE(IsToastShown(id1));

        EXPECT_EQ(r0.width(), r1.width());
        EXPECT_EQ(r0.height(), r1.height());
        EXPECT_GT(r0.y(), r1.y());
        EXPECT_EQ(r0.x(), r1.x());

        // Ensure that toasts are on the left.
        EXPECT_LT(r1.x(), GetWorkArea().CenterPoint().x());

        CloseAllToasts();
        EXPECT_EQ(0u, GetToastCounts());
        WaitForTransitionsDone();

        // Restore simulated taskbar position to bottom.
        SetDisplayInfo(gfx::Rect(0, 0, 600, 390), // Work-area.
            gfx::Rect(0, 0, 600, 400)); // Display-bounds.
    }

    TEST_F(MessagePopupCollectionTest, DetectMouseHover)
    {
        std::string id0 = AddNotification();
        std::string id1 = AddNotification();
        WaitForTransitionsDone();

        views::WidgetDelegateView* toast0 = GetToast(id0);
        EXPECT_TRUE(toast0 != NULL);
        views::WidgetDelegateView* toast1 = GetToast(id1);
        EXPECT_TRUE(toast1 != NULL);

        ui::MouseEvent event(ui::ET_MOUSE_MOVED, gfx::Point(), gfx::Point(),
            ui::EventTimeForNow(), 0, 0);

        // Test that mouse detection logic works in presence of out-of-order events.
        toast0->OnMouseEntered(event);
        EXPECT_TRUE(MouseInCollection());
        toast1->OnMouseEntered(event);
        EXPECT_TRUE(MouseInCollection());
        toast0->OnMouseExited(event);
        EXPECT_TRUE(MouseInCollection());
        toast1->OnMouseExited(event);
        EXPECT_FALSE(MouseInCollection());

        // Test that mouse detection logic works in presence of WindowClosing events.
        toast0->OnMouseEntered(event);
        EXPECT_TRUE(MouseInCollection());
        toast1->OnMouseEntered(event);
        EXPECT_TRUE(MouseInCollection());
        toast0->GetWidget()->CloseNow();
        EXPECT_TRUE(MouseInCollection());
        toast1->GetWidget()->CloseNow();
        EXPECT_FALSE(MouseInCollection());
    }

    // TODO(dimich): Test repositioning - both normal one and when user is closing
    // the toasts.
    TEST_F(MessagePopupCollectionTest, DetectMouseHoverWithUserClose)
    {
        std::string id0 = AddNotification();
        std::string id1 = AddNotification();
        WaitForTransitionsDone();

        views::WidgetDelegateView* toast0 = GetToast(id0);
        EXPECT_TRUE(toast0 != NULL);
        views::WidgetDelegateView* toast1 = GetToast(id1);
        ASSERT_TRUE(toast1 != NULL);

        ui::MouseEvent event(ui::ET_MOUSE_MOVED, gfx::Point(), gfx::Point(),
            ui::EventTimeForNow(), 0, 0);
        toast1->OnMouseEntered(event);
        static_cast<MessageCenterObserver*>(collection())->OnNotificationRemoved(id1, true);

        EXPECT_FALSE(MouseInCollection());
        std::string id2 = AddNotification();

        WaitForTransitionsDone();
        views::WidgetDelegateView* toast2 = GetToast(id2);
        EXPECT_TRUE(toast2 != NULL);

        CloseAllToasts();
        WaitForTransitionsDone();
    }

    TEST_F(MessagePopupCollectionTest, ManyPopupNotifications)
    {
        // Add the max visible popup notifications +1, ensure the correct num visible.
        size_t notifications_to_add = 3 + 1;
        std::vector<std::string> ids(notifications_to_add);
        for (size_t i = 0; i < notifications_to_add; ++i) {
            ids[i] = AddNotification();
        }

        WaitForTransitionsDone();

        for (size_t i = 0; i < notifications_to_add - 1; ++i) {
            EXPECT_TRUE(IsToastShown(ids[i])) << "Should show the " << i << "th ID";
        }
        EXPECT_FALSE(IsToastShown(ids[notifications_to_add - 1]));

        CloseAllToasts();
        WaitForTransitionsDone();
    }

} // namespace test
} // namespace message_center
